feat: serve content-hashed chunks outside BUILD_ID path#3779
feat: serve content-hashed chunks outside BUILD_ID path#3779bartlomieju wants to merge 4 commits intomainfrom
Conversation
…ching
esbuild already content-hashes shared chunks and assets (chunk-XXXX.js,
file-XXXX.wasm), but Fresh served them under /_fresh/js/{BUILD_ID}/.
This meant every deploy invalidated all cached chunks — even unchanged
ones like large WASM files.
Move shared chunks/assets to /_fresh/js/c/ (outside the BUILD_ID
directory) so their URLs only change when their content changes. Entry
points (fresh-runtime, islands) remain under /_fresh/js/{BUILD_ID}/.
Adds a `contentAddressedStatic` glob pattern option to BuildOptions. Matching static files use their content hash (instead of BUILD_ID) as the asset() cache-bust key, so their URLs survive deploys unchanged. - asset() checks a content hash registry for matching files - ProdBuildCache populates the registry from snapshot on startup - Middleware accepts content hash as a valid cache-bust key - Matching files get immutable cache headers in production
Port the contentAddressedStatic option to FreshViteConfig so it works with the modern App/Vite path (not just the legacy Builder API). Static files matching the glob patterns are marked immutable in the snapshot, enabling content-hash caching that survives deploys.
bartlomieju
left a comment
There was a problem hiding this comment.
Overall: Clean PR, well-structured. Two distinct but related features: (1) esbuild shared chunks always go to /_fresh/js/c/ (automatic), (2) user static files opt in via contentAddressedStatic globs. Good test coverage and docs.
Issues:
-
Glob matching inconsistency — In
dev_build_cache.ts:363,isContentAddressed(name)is called wherenameis a URL-style pathname (e.g.,/large.wasm). Inserver_snapshot.ts:284,isContentAddressed(relative)uses a filesystem-relative path (e.g.,large.wasm, no leading slash). The same user-provided glob pattern like**/*.wasmmay match one but not the other depending on howglobToRegExphandles the leading/. Worth adding a test that exercises both paths with the same pattern, or normalizing the input before matching. -
Source maps for relocated chunks — The test plan has "Verify source maps resolve correctly for relocated chunks" unchecked. Since chunks move from
/_fresh/js/{BUILD_ID}/to/_fresh/js/c/, the//# sourceMappingURL=comment esbuild emits is relative. If the.mapfile also lands under../c/it should be fine, but this is worth verifying before merge — broken source maps in production are painful to debug after the fact. -
file.hash !== cacheKeybroadens valid cache keys (static_files.ts:68) — Before this change, onlyBUILD_IDwas accepted as a cache key. Now any file's content hash is also accepted. This is correct for content-addressed files, but it also applies to all static files — if a request comes in with a matching hash for a non-content-addressed file, it'll get immutable caching too. This is probably fine (correct hash = correct content), but it's a subtle behavioral change worth calling out in the PR description.
Nits:
-
The
|| undefinedpattern indev_build_cache.ts:366andserver_snapshot.ts:322(immutable: isContentAddressed(name) || undefined) is a bit subtle — a short comment like// omit false to keep default behaviorwould help. -
The
"../c/"string appears in bothesbuild.tsandbuilder.ts— consider extracting it as a shared constant to keep them in sync.
Summary
chunk-XXXX.js), but were served under/_fresh/js/{BUILD_ID}/— meaning every deploy invalidated all cached chunks, even unchanged oneschunkNames/assetNamesoptions to output shared chunks to../c/, which the builder maps to/_fresh/js/c/— outside the BUILD_ID directory/_fresh/js/{BUILD_ID}/since they don't have content hashes in their filenames/_fresh/js/c/as immutable, so browsers cache these files across deploysBefore:
/_fresh/js/{BUILD_ID}/chunk-abc123.js— URL changes every deploy even if content didn't changeAfter:
/_fresh/js/c/chunk-abc123.js— URL only changes when content changesThis is especially impactful for large assets like WASM files that rarely change but are expensive to re-download.
Test plan
/_fresh/js/c/paths (both JS chunks and WASM assets)